This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 720358425bc7 CAMEL-25889: camel-google-mail/drive/calendar/sheets - 
create the API client per endpoint credentials instead of one per component 
(#28217)
720358425bc7 is described below

commit 720358425bc7dd31b7dff533f49a0b7914e1556f
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Oct 22 22:45:27 2026 +0200

    CAMEL-25889: camel-google-mail/drive/calendar/sheets - create the API 
client per endpoint credentials instead of one per component (#28217)
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
---
 .../camel/catalog/docs/google-mail-component.adoc  |   2 +
 .../google/calendar/GoogleCalendarComponent.java   |  33 +++---
 .../google/calendar/GoogleCalendarEndpoint.java    |  22 +++-
 .../stream/GoogleCalendarStreamComponent.java      |  31 +++--
 .../stream/GoogleCalendarStreamEndpoint.java       |  22 +++-
 ...GoogleCalendarClientPerEndpointOptionsTest.java | 104 +++++++++++++++++
 ...CalendarStreamClientPerEndpointOptionsTest.java | 109 ++++++++++++++++++
 .../google/drive/GoogleDriveComponent.java         |  42 ++++---
 .../google/drive/GoogleDriveEndpoint.java          |  22 +++-
 .../GoogleDriveClientPerEndpointOptionsTest.java   | 107 +++++++++++++++++
 .../src/main/docs/google-mail-component.adoc       |   2 +
 .../component/google/mail/GoogleMailComponent.java |  63 +++++++---
 .../component/google/mail/GoogleMailEndpoint.java  |  22 +++-
 .../mail/stream/GoogleMailStreamComponent.java     |  32 +++---
 .../mail/stream/GoogleMailStreamEndpoint.java      |  22 +++-
 ...MailUpdateMessageLabelsDataTypeTransformer.java |   2 +-
 .../GoogleMailClientPerEndpointOptionsTest.java    | 127 +++++++++++++++++++++
 ...ogleMailStreamClientPerEndpointOptionsTest.java | 124 ++++++++++++++++++++
 .../google/sheets/GoogleSheetsComponent.java       |  26 ++---
 .../google/sheets/GoogleSheetsEndpoint.java        |  22 +++-
 .../sheets/stream/GoogleSheetsStreamComponent.java |  24 ++--
 .../sheets/stream/GoogleSheetsStreamEndpoint.java  |  23 +++-
 .../GoogleSheetsClientPerEndpointOptionsTest.java  | 103 +++++++++++++++++
 ...leSheetsStreamClientPerEndpointOptionsTest.java | 109 ++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  15 +++
 25 files changed, 1095 insertions(+), 115 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-mail-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-mail-component.adoc
index eeb80e952985..52dd7ab52523 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-mail-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-mail-component.adoc
@@ -77,6 +77,8 @@ The `google-mail:update-message-labels` data type transformer 
builds a `ModifyMe
 
 This is useful for modifying Gmail message labels (categorizing, archiving, 
starring, moving, etc.) without manually calling the labels/list API.
 
+The transformer calls the Gmail API with the credentials configured on the 
`google-mail` component (for example `camel.component.google-mail.clientId`, 
`clientSecret` and `refreshToken`, or `serviceAccountKey` and `delegate`). 
Credentials set only on an endpoint URI do not apply to the transformer.
+
 === Exchange Variables
 
 [cols="1,1,3"]
diff --git 
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarComponent.java
 
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarComponent.java
index 902a6fc68534..960bbdfc263f 100644
--- 
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarComponent.java
+++ 
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarComponent.java
@@ -32,8 +32,6 @@ public class GoogleCalendarComponent
     @Metadata
     GoogleCalendarConfiguration configuration;
     @Metadata(label = "advanced")
-    private Calendar client;
-    @Metadata(label = "advanced")
     private GoogleCalendarClientFactory clientFactory;
 
     public GoogleCalendarComponent() {
@@ -49,23 +47,24 @@ public class GoogleCalendarComponent
         return 
getCamelContext().getTypeConverter().convertTo(GoogleCalendarApiName.class, 
apiNameStr);
     }
 
+    /**
+     * Creates a client for the given configuration with the client factory of 
the component. Each endpoint creates its
+     * own client from its own options and keeps it for its lifetime.
+     */
     public Calendar getClient(GoogleCalendarConfiguration config) {
-        if (client == null) {
-            if (config.getClientId() != null && !config.getClientId().isBlank()
-                    && config.getClientSecret() != null && 
!config.getClientSecret().isBlank()) {
-                client = getClientFactory().makeClient(config.getClientId(), 
config.getClientSecret(), config.getScopesAsList(),
-                        config.getApplicationName(), config.getRefreshToken(),
-                        config.getAccessToken(), config.getEmailAddress(), 
config.getP12FileName(), config.getUser());
-            } else if (config.getServiceAccountKey() != null && 
!config.getServiceAccountKey().isBlank()) {
-                client = getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
-                        config.getScopesAsList(),
-                        config.getApplicationName(), config.getDelegate());
-            } else {
-                throw new IllegalArgumentException(
-                        "(clientId and clientSecret) or serviceAccountKey are 
required to create Google Calendar client");
-            }
+        if (config.getClientId() != null && !config.getClientId().isBlank()
+                && config.getClientSecret() != null && 
!config.getClientSecret().isBlank()) {
+            return getClientFactory().makeClient(config.getClientId(), 
config.getClientSecret(), config.getScopesAsList(),
+                    config.getApplicationName(), config.getRefreshToken(),
+                    config.getAccessToken(), config.getEmailAddress(), 
config.getP12FileName(), config.getUser());
+        } else if (config.getServiceAccountKey() != null && 
!config.getServiceAccountKey().isBlank()) {
+            return getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
+                    config.getScopesAsList(),
+                    config.getApplicationName(), config.getDelegate());
+        } else {
+            throw new IllegalArgumentException(
+                    "(clientId and clientSecret) or serviceAccountKey are 
required to create Google Calendar client");
         }
-        return client;
     }
 
     public GoogleCalendarClientFactory getClientFactory() {
diff --git 
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java
 
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java
index ce1874bdf544..18bb41040a40 100644
--- 
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java
+++ 
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/GoogleCalendarEndpoint.java
@@ -17,6 +17,8 @@
 package org.apache.camel.component.google.calendar;
 
 import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import com.google.api.services.calendar.Calendar;
 import org.apache.camel.Category;
@@ -46,6 +48,8 @@ public class GoogleCalendarEndpoint extends 
AbstractApiEndpoint<GoogleCalendarAp
 
     @UriParam
     private GoogleCalendarConfiguration configuration;
+    private final Lock clientLock = new ReentrantLock();
+    private volatile Calendar client;
 
     private Object apiProxy;
 
@@ -116,8 +120,24 @@ public class GoogleCalendarEndpoint extends 
AbstractApiEndpoint<GoogleCalendarAp
         }
     }
 
+    /**
+     * The client of this endpoint, created on first use from the options of 
this endpoint and kept for the lifetime of
+     * the endpoint.
+     */
     public Calendar getClient() {
-        return ((GoogleCalendarComponent) 
getComponent()).getClient(configuration);
+        Calendar answer = client;
+        if (answer == null) {
+            clientLock.lock();
+            try {
+                if (client == null) {
+                    client = ((GoogleCalendarComponent) 
getComponent()).getClient(configuration);
+                }
+                answer = client;
+            } finally {
+                clientLock.unlock();
+            }
+        }
+        return answer;
     }
 
     @Override
diff --git 
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamComponent.java
 
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamComponent.java
index e02ccb129489..2091af64b47f 100644
--- 
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamComponent.java
+++ 
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamComponent.java
@@ -30,8 +30,6 @@ import org.apache.camel.support.HealthCheckComponent;
 @Component("google-calendar-stream")
 public class GoogleCalendarStreamComponent extends HealthCheckComponent {
 
-    @Metadata(label = "advanced")
-    private Calendar client;
     @Metadata(label = "advanced")
     private GoogleCalendarClientFactory clientFactory;
     @Metadata
@@ -47,22 +45,23 @@ public class GoogleCalendarStreamComponent extends 
HealthCheckComponent {
         this.configuration = new GoogleCalendarStreamConfiguration();
     }
 
+    /**
+     * Creates a client for the given configuration with the client factory of 
the component. Each endpoint creates its
+     * own client from its own options and keeps it for its lifetime.
+     */
     public Calendar getClient(GoogleCalendarStreamConfiguration config) {
-        if (client == null) {
-            if (config.getClientId() != null && config.getClientSecret() != 
null) {
-                client = getClientFactory().makeClient(config.getClientId(), 
config.getClientSecret(), config.getScopesAsList(),
-                        config.getApplicationName(), config.getRefreshToken(),
-                        config.getAccessToken(), config.getEmailAddress(), 
config.getP12FileName(), config.getUser());
-            } else if (config.getServiceAccountKey() != null) {
-                client = getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
-                        config.getScopesAsList(),
-                        config.getApplicationName(), config.getDelegate());
-            } else {
-                throw new IllegalArgumentException(
-                        "(clientId and clientSecret) or serviceAccountKey are 
required to create Gmail client");
-            }
+        if (config.getClientId() != null && config.getClientSecret() != null) {
+            return getClientFactory().makeClient(config.getClientId(), 
config.getClientSecret(), config.getScopesAsList(),
+                    config.getApplicationName(), config.getRefreshToken(),
+                    config.getAccessToken(), config.getEmailAddress(), 
config.getP12FileName(), config.getUser());
+        } else if (config.getServiceAccountKey() != null) {
+            return getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
+                    config.getScopesAsList(),
+                    config.getApplicationName(), config.getDelegate());
+        } else {
+            throw new IllegalArgumentException(
+                    "(clientId and clientSecret) or serviceAccountKey are 
required to create Google Calendar client");
         }
-        return client;
     }
 
     /**
diff --git 
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamEndpoint.java
 
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamEndpoint.java
index c14d6783bde5..5dbd2b3e9cba 100644
--- 
a/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamEndpoint.java
+++ 
b/components/camel-google/camel-google-calendar/src/main/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamEndpoint.java
@@ -17,6 +17,8 @@
 package org.apache.camel.component.google.calendar.stream;
 
 import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import com.google.api.services.calendar.Calendar;
 import com.google.api.services.calendar.model.Event;
@@ -46,6 +48,8 @@ public class GoogleCalendarStreamEndpoint extends 
ScheduledPollEndpoint implemen
 
     @UriParam
     private GoogleCalendarStreamConfiguration configuration;
+    private final Lock clientLock = new ReentrantLock();
+    private volatile Calendar client;
 
     public GoogleCalendarStreamEndpoint(String uri, 
GoogleCalendarStreamComponent component,
                                         GoogleCalendarStreamConfiguration 
endpointConfiguration) {
@@ -75,8 +79,24 @@ public class GoogleCalendarStreamEndpoint extends 
ScheduledPollEndpoint implemen
         return consumer;
     }
 
+    /**
+     * The client of this endpoint, created on first use from the options of 
this endpoint and kept for the lifetime of
+     * the endpoint.
+     */
     public Calendar getClient() {
-        return ((GoogleCalendarStreamComponent) 
getComponent()).getClient(configuration);
+        Calendar answer = client;
+        if (answer == null) {
+            clientLock.lock();
+            try {
+                if (client == null) {
+                    client = ((GoogleCalendarStreamComponent) 
getComponent()).getClient(configuration);
+                }
+                answer = client;
+            } finally {
+                clientLock.unlock();
+            }
+        }
+        return answer;
     }
 
     public GoogleCalendarStreamConfiguration getConfiguration() {
diff --git 
a/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/GoogleCalendarClientPerEndpointOptionsTest.java
 
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/GoogleCalendarClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..39563caaed2a
--- /dev/null
+++ 
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/GoogleCalendarClientPerEndpointOptionsTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.google.calendar;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.calendar.Calendar;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleCalendarClientPerEndpointOptionsTest {
+
+    private static final String OAUTH = 
"?clientId=id&clientSecret=secret&refreshToken=";
+    private static final String SERVICE_ACCOUNT = 
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+    @Test
+    void shouldCreateOneClientPerEndpoint() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-calendar", 
GoogleCalendarComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleCalendarEndpoint first = 
context.getEndpoint("google-calendar://calendars/get" + OAUTH + "token-a",
+                    GoogleCalendarEndpoint.class);
+            GoogleCalendarEndpoint second = 
context.getEndpoint("google-calendar://calendars/get" + OAUTH + "token-b",
+                    GoogleCalendarEndpoint.class);
+            GoogleCalendarEndpoint sameAsFirst = context.getEndpoint(
+                    "google-calendar://calendars/insert" + OAUTH + "token-a", 
GoogleCalendarEndpoint.class);
+
+            assertEquals(Arrays.asList("token-a", "token-b", "token-a"), 
factory.created);
+            assertNotSame(first.getClient(), second.getClient());
+            assertNotSame(first.getClient(), sameAsFirst.getClient());
+            assertSame(first.getClient(), first.getClient());
+            assertEquals(3, factory.created.size());
+        }
+    }
+
+    @Test
+    void shouldKeepOneClientPerDelegate() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-calendar", 
GoogleCalendarComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleCalendarEndpoint first = context.getEndpoint(
+                    "google-calendar://calendars/get" + SERVICE_ACCOUNT + 
"user-a", GoogleCalendarEndpoint.class);
+            GoogleCalendarEndpoint second = context.getEndpoint(
+                    "google-calendar://calendars/get" + SERVICE_ACCOUNT + 
"user-b", GoogleCalendarEndpoint.class);
+
+            assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+            assertNotSame(first.getClient(), second.getClient());
+        }
+    }
+
+    private static final class RecordingClientFactory implements 
GoogleCalendarClientFactory {
+
+        private final List<String> created = new ArrayList<>();
+
+        @Override
+        public Calendar makeClient(
+                String clientId, String clientSecret, Collection<String> 
scopes, String applicationName,
+                String refreshToken, String accessToken, String emailAddress, 
String p12FileName, String user) {
+            created.add(refreshToken);
+            return newClient();
+        }
+
+        @Override
+        public Calendar makeClient(
+                CamelContext camelContext, String serviceAccountKey, 
Collection<String> scopes, String applicationName,
+                String delegate) {
+            created.add(delegate);
+            return newClient();
+        }
+
+        private static Calendar newClient() {
+            return new Calendar.Builder(new NetHttpTransport(), new 
JacksonFactory(), null).setApplicationName("test")
+                    .build();
+        }
+    }
+}
diff --git 
a/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamClientPerEndpointOptionsTest.java
 
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..220e5f52ba52
--- /dev/null
+++ 
b/components/camel-google/camel-google-calendar/src/test/java/org/apache/camel/component/google/calendar/stream/GoogleCalendarStreamClientPerEndpointOptionsTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.google.calendar.stream;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.calendar.Calendar;
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.google.calendar.GoogleCalendarClientFactory;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleCalendarStreamClientPerEndpointOptionsTest {
+
+    private static final String OAUTH = 
"?clientId=id&clientSecret=secret&refreshToken=";
+    private static final String SERVICE_ACCOUNT = 
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+    @Test
+    void shouldCreateOneClientPerEndpoint() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-calendar-stream", 
GoogleCalendarStreamComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleCalendarStreamEndpoint first = context.getEndpoint(
+                    "google-calendar-stream://cal-a" + OAUTH + "token-a", 
GoogleCalendarStreamEndpoint.class);
+            GoogleCalendarStreamEndpoint second = context.getEndpoint(
+                    "google-calendar-stream://cal-b" + OAUTH + "token-b", 
GoogleCalendarStreamEndpoint.class);
+            GoogleCalendarStreamEndpoint sameAsFirst = context.getEndpoint(
+                    "google-calendar-stream://cal-c" + OAUTH + "token-a", 
GoogleCalendarStreamEndpoint.class);
+
+            Calendar firstClient = first.getClient();
+            Calendar secondClient = second.getClient();
+
+            assertNotSame(firstClient, secondClient);
+            assertNotSame(firstClient, sameAsFirst.getClient());
+            assertSame(firstClient, first.getClient());
+            assertEquals(Arrays.asList("token-a", "token-b", "token-a"), 
factory.created);
+        }
+    }
+
+    @Test
+    void shouldKeepOneClientPerDelegate() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-calendar-stream", 
GoogleCalendarStreamComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleCalendarStreamEndpoint first
+                    = context.getEndpoint("google-calendar-stream://cal-a" + 
SERVICE_ACCOUNT + "user-a",
+                            GoogleCalendarStreamEndpoint.class);
+            GoogleCalendarStreamEndpoint second
+                    = context.getEndpoint("google-calendar-stream://cal-b" + 
SERVICE_ACCOUNT + "user-b",
+                            GoogleCalendarStreamEndpoint.class);
+
+            assertNotSame(first.getClient(), second.getClient());
+            assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+        }
+    }
+
+    private static final class RecordingClientFactory implements 
GoogleCalendarClientFactory {
+
+        private final List<String> created = new ArrayList<>();
+
+        @Override
+        public Calendar makeClient(
+                String clientId, String clientSecret, Collection<String> 
scopes, String applicationName,
+                String refreshToken, String accessToken, String emailAddress, 
String p12FileName, String user) {
+            created.add(refreshToken);
+            return newClient();
+        }
+
+        @Override
+        public Calendar makeClient(
+                CamelContext camelContext, String serviceAccountKey, 
Collection<String> scopes, String applicationName,
+                String delegate) {
+            created.add(delegate);
+            return newClient();
+        }
+
+        private static Calendar newClient() {
+            return new Calendar.Builder(new NetHttpTransport(), new 
JacksonFactory(), null).setApplicationName("test")
+                    .build();
+        }
+    }
+}
diff --git 
a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java
 
b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java
index 74d14d010355..83a3b16feb44 100644
--- 
a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java
+++ 
b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveComponent.java
@@ -37,8 +37,6 @@ public class GoogleDriveComponent
     @Metadata
     GoogleDriveConfiguration configuration;
     @Metadata(label = "advanced")
-    private Drive client;
-    @Metadata(label = "advanced")
     private GoogleDriveClientFactory clientFactory;
     @Metadata(label = "proxy", description = "Proxy server host")
     private String proxyHost;
@@ -58,22 +56,34 @@ public class GoogleDriveComponent
         return 
getCamelContext().getTypeConverter().convertTo(GoogleDriveApiName.class, 
apiNameStr);
     }
 
+    /**
+     * Creates a client for the given configuration with the client factory of 
the component.
+     *
+     * @see #getClient(GoogleDriveConfiguration, GoogleDriveClientFactory)
+     */
     public Drive getClient(GoogleDriveConfiguration config) {
-        if (client == null) {
-            if (config.getClientId() != null && !config.getClientId().isBlank()
-                    && config.getClientSecret() != null && 
!config.getClientSecret().isBlank()) {
-                client = getClientFactory().makeClient(config.getClientId(),
-                        config.getClientSecret(), config.getScopesAsList(),
-                        config.getApplicationName(), config.getRefreshToken(), 
config.getAccessToken());
-            } else if (config.getServiceAccountKey() != null && 
!config.getServiceAccountKey().isBlank()) {
-                client = getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
-                        config.getScopesAsList(), config.getApplicationName(), 
config.getDelegate());
-            } else {
-                throw new IllegalArgumentException(
-                        "(clientId and clientSecret) or serviceAccountKey are 
required to create Google Drive client");
-            }
+        return getClient(config, null);
+    }
+
+    /**
+     * Creates a client for the given configuration with the given client 
factory or, when that is null, with the client
+     * factory of the component. Each endpoint creates its own client from its 
own options and keeps it for its
+     * lifetime.
+     */
+    public Drive getClient(GoogleDriveConfiguration config, 
GoogleDriveClientFactory factory) {
+        GoogleDriveClientFactory factoryToUse = factory != null ? factory : 
getClientFactory();
+        if (config.getClientId() != null && !config.getClientId().isBlank()
+                && config.getClientSecret() != null && 
!config.getClientSecret().isBlank()) {
+            return factoryToUse.makeClient(config.getClientId(),
+                    config.getClientSecret(), config.getScopesAsList(),
+                    config.getApplicationName(), config.getRefreshToken(), 
config.getAccessToken());
+        } else if (config.getServiceAccountKey() != null && 
!config.getServiceAccountKey().isBlank()) {
+            return factoryToUse.makeClient(getCamelContext(), 
config.getServiceAccountKey(),
+                    config.getScopesAsList(), config.getApplicationName(), 
config.getDelegate());
+        } else {
+            throw new IllegalArgumentException(
+                    "(clientId and clientSecret) or serviceAccountKey are 
required to create Google Drive client");
         }
-        return client;
     }
 
     public GoogleDriveClientFactory getClientFactory() {
diff --git 
a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java
 
b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java
index 9a428f77400b..def2ac80d3c5 100644
--- 
a/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java
+++ 
b/components/camel-google/camel-google-drive/src/main/java/org/apache/camel/component/google/drive/GoogleDriveEndpoint.java
@@ -17,6 +17,8 @@
 package org.apache.camel.component.google.drive;
 
 import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import com.google.api.services.drive.Drive;
 import org.apache.camel.*;
@@ -44,6 +46,8 @@ public class GoogleDriveEndpoint extends 
AbstractApiEndpoint<GoogleDriveApiName,
 
     @UriParam
     private GoogleDriveConfiguration configuration;
+    private final Lock clientLock = new ReentrantLock();
+    private volatile Drive client;
 
     @UriParam
     private GoogleDriveClientFactory clientFactory;
@@ -120,8 +124,24 @@ public class GoogleDriveEndpoint extends 
AbstractApiEndpoint<GoogleDriveApiName,
         }
     }
 
+    /**
+     * The client of this endpoint, created on first use from the options of 
this endpoint and kept for the lifetime of
+     * the endpoint.
+     */
     public Drive getClient() {
-        return ((GoogleDriveComponent) 
getComponent()).getClient(configuration);
+        Drive answer = client;
+        if (answer == null) {
+            clientLock.lock();
+            try {
+                if (client == null) {
+                    client = ((GoogleDriveComponent) 
getComponent()).getClient(configuration, clientFactory);
+                }
+                answer = client;
+            } finally {
+                clientLock.unlock();
+            }
+        }
+        return answer;
     }
 
     @Override
diff --git 
a/components/camel-google/camel-google-drive/src/test/java/org/apache/camel/component/google/drive/GoogleDriveClientPerEndpointOptionsTest.java
 
b/components/camel-google/camel-google-drive/src/test/java/org/apache/camel/component/google/drive/GoogleDriveClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..ea99736905fb
--- /dev/null
+++ 
b/components/camel-google/camel-google-drive/src/test/java/org/apache/camel/component/google/drive/GoogleDriveClientPerEndpointOptionsTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.google.drive;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.drive.Drive;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleDriveClientPerEndpointOptionsTest {
+
+    private static final String OAUTH = 
"?clientId=id&clientSecret=secret&refreshToken=";
+
+    @Test
+    void shouldCreateOneClientPerEndpoint() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-drive", 
GoogleDriveComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleDriveEndpoint first
+                    = context.getEndpoint("google-drive://drive-files/list" + 
OAUTH + "token-a", GoogleDriveEndpoint.class);
+            GoogleDriveEndpoint second
+                    = context.getEndpoint("google-drive://drive-files/list" + 
OAUTH + "token-b", GoogleDriveEndpoint.class);
+            GoogleDriveEndpoint sameAsFirst
+                    = context.getEndpoint("google-drive://drive-files/get" + 
OAUTH + "token-a", GoogleDriveEndpoint.class);
+
+            assertEquals(Arrays.asList("token-a", "token-b", "token-a"), 
factory.created);
+            assertNotSame(first.getClient(), second.getClient());
+            assertNotSame(first.getClient(), sameAsFirst.getClient());
+            assertSame(first.getClient(), first.getClient());
+            assertEquals(3, factory.created.size());
+        }
+    }
+
+    @Test
+    void shouldUseTheClientFactoryOfTheEndpoint() throws Exception {
+        RecordingClientFactory componentFactory = new RecordingClientFactory();
+        RecordingClientFactory endpointFactory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getRegistry().bind("endpointFactory", endpointFactory);
+            context.getComponent("google-drive", 
GoogleDriveComponent.class).setClientFactory(componentFactory);
+            context.start();
+
+            GoogleDriveEndpoint withOwnFactory = context.getEndpoint(
+                    "google-drive://drive-files/list" + OAUTH + 
"token-a&clientFactory=#endpointFactory",
+                    GoogleDriveEndpoint.class);
+            GoogleDriveEndpoint withComponentFactory
+                    = context.getEndpoint("google-drive://drive-files/get" + 
OAUTH + "token-a", GoogleDriveEndpoint.class);
+
+            assertEquals(List.of("token-a"), endpointFactory.created);
+            assertEquals(List.of("token-a"), componentFactory.created);
+            assertNotSame(withOwnFactory.getClient(), 
withComponentFactory.getClient());
+        }
+    }
+
+    private static final class RecordingClientFactory implements 
GoogleDriveClientFactory {
+
+        private final List<String> created = new ArrayList<>();
+
+        @Override
+        public Drive makeClient(
+                String clientId, String clientSecret, Collection<String> 
scopes, String applicationName,
+                String refreshToken, String accessToken) {
+            created.add(refreshToken);
+            return newClient();
+        }
+
+        @Override
+        public Drive makeClient(
+                CamelContext camelContext, String serviceAccountKey, 
Collection<String> scopes, String applicationName,
+                String delegate) {
+            created.add(delegate);
+            return newClient();
+        }
+
+        private static Drive newClient() {
+            return new Drive.Builder(new NetHttpTransport(), new 
JacksonFactory(), null).setApplicationName("test")
+                    .build();
+        }
+    }
+}
diff --git 
a/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
 
b/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
index eeb80e952985..52dd7ab52523 100644
--- 
a/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
+++ 
b/components/camel-google/camel-google-mail/src/main/docs/google-mail-component.adoc
@@ -77,6 +77,8 @@ The `google-mail:update-message-labels` data type transformer 
builds a `ModifyMe
 
 This is useful for modifying Gmail message labels (categorizing, archiving, 
starring, moving, etc.) without manually calling the labels/list API.
 
+The transformer calls the Gmail API with the credentials configured on the 
`google-mail` component (for example `camel.component.google-mail.clientId`, 
`clientSecret` and `refreshToken`, or `serviceAccountKey` and `delegate`). 
Credentials set only on an endpoint URI do not apply to the transformer.
+
 === Exchange Variables
 
 [cols="1,1,3"]
diff --git 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailComponent.java
 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailComponent.java
index 10771cc95b0a..009da6c7de62 100644
--- 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailComponent.java
+++ 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailComponent.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.component.google.mail;
 
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
 import com.google.api.services.gmail.Gmail;
 import org.apache.camel.CamelContext;
 import org.apache.camel.Endpoint;
@@ -32,9 +35,9 @@ public class GoogleMailComponent
     @Metadata
     GoogleMailConfiguration configuration;
     @Metadata(label = "advanced")
-    private Gmail client;
-    @Metadata(label = "advanced")
     private GoogleMailClientFactory clientFactory;
+    private final Lock clientLock = new ReentrantLock();
+    private volatile Gmail client;
 
     public GoogleMailComponent() {
         super(GoogleMailApiName.class, 
GoogleMailApiCollection.getCollection());
@@ -49,22 +52,44 @@ public class GoogleMailComponent
         return 
getCamelContext().getTypeConverter().convertTo(GoogleMailApiName.class, 
apiNameStr);
     }
 
-    public Gmail getClient(GoogleMailConfiguration config) {
-        if (client == null) {
-            if (config.getClientId() != null && !config.getClientId().isBlank()
-                    && config.getClientSecret() != null && 
!config.getClientSecret().isBlank()) {
-                client = getClientFactory().makeClient(config.getClientId(),
-                        config.getClientSecret(), config.getScopesAsList(),
-                        config.getApplicationName(), config.getRefreshToken(), 
config.getAccessToken());
-            } else if (config.getServiceAccountKey() != null && 
!config.getServiceAccountKey().isBlank()) {
-                client = getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
-                        config.getScopesAsList(), config.getApplicationName(), 
config.getDelegate());
-            } else {
-                throw new IllegalArgumentException(
-                        "(clientId and clientSecret) or serviceAccountKey are 
required to create Gmail client");
+    /**
+     * Gets the client for the configuration of the component, created on 
first use and kept until the component stops.
+     * It serves callers that have no endpoint, such as the {@code 
google-mail:update-message-labels} data type
+     * transformer. Endpoints do not use it: each endpoint creates its own 
client from its own options.
+     */
+    public Gmail getClient() {
+        Gmail answer = client;
+        if (answer == null) {
+            clientLock.lock();
+            try {
+                if (client == null) {
+                    client = getClient(getConfiguration());
+                }
+                answer = client;
+            } finally {
+                clientLock.unlock();
             }
         }
-        return client;
+        return answer;
+    }
+
+    /**
+     * Creates a client for the given configuration with the client factory of 
the component. Each endpoint creates its
+     * own client from its own options and keeps it for its lifetime.
+     */
+    public Gmail getClient(GoogleMailConfiguration config) {
+        if (config.getClientId() != null && !config.getClientId().isBlank()
+                && config.getClientSecret() != null && 
!config.getClientSecret().isBlank()) {
+            return getClientFactory().makeClient(config.getClientId(),
+                    config.getClientSecret(), config.getScopesAsList(),
+                    config.getApplicationName(), config.getRefreshToken(), 
config.getAccessToken());
+        } else if (config.getServiceAccountKey() != null && 
!config.getServiceAccountKey().isBlank()) {
+            return getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
+                    config.getScopesAsList(), config.getApplicationName(), 
config.getDelegate());
+        } else {
+            throw new IllegalArgumentException(
+                    "(clientId and clientSecret) or serviceAccountKey are 
required to create Gmail client");
+        }
     }
 
     public GoogleMailClientFactory getClientFactory() {
@@ -105,4 +130,10 @@ public class GoogleMailComponent
         endpointConfiguration.setMethodName(methodName);
         return new GoogleMailEndpoint(uri, this, apiName, methodName, 
endpointConfiguration);
     }
+
+    @Override
+    protected void doStop() throws Exception {
+        client = null;
+        super.doStop();
+    }
 }
diff --git 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailEndpoint.java
 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailEndpoint.java
index 979c23067f2e..91887d07f231 100644
--- 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailEndpoint.java
+++ 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/GoogleMailEndpoint.java
@@ -17,6 +17,8 @@
 package org.apache.camel.component.google.mail;
 
 import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import com.google.api.services.gmail.Gmail;
 import org.apache.camel.Category;
@@ -52,6 +54,8 @@ public class GoogleMailEndpoint extends 
AbstractApiEndpoint<GoogleMailApiName, G
 
     @UriParam
     private GoogleMailConfiguration configuration;
+    private final Lock clientLock = new ReentrantLock();
+    private volatile Gmail client;
 
     public GoogleMailEndpoint(String uri, GoogleMailComponent component, 
GoogleMailApiName apiName, String methodName,
                               GoogleMailConfiguration endpointConfiguration) {
@@ -116,8 +120,24 @@ public class GoogleMailEndpoint extends 
AbstractApiEndpoint<GoogleMailApiName, G
         }
     }
 
+    /**
+     * The client of this endpoint, created on first use from the options of 
this endpoint and kept for the lifetime of
+     * the endpoint.
+     */
     public Gmail getClient() {
-        return ((GoogleMailComponent) getComponent()).getClient(configuration);
+        Gmail answer = client;
+        if (answer == null) {
+            clientLock.lock();
+            try {
+                if (client == null) {
+                    client = ((GoogleMailComponent) 
getComponent()).getClient(configuration);
+                }
+                answer = client;
+            } finally {
+                clientLock.unlock();
+            }
+        }
+        return answer;
     }
 
     @Override
diff --git 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamComponent.java
 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamComponent.java
index b34760b7621f..97f96b453983 100644
--- 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamComponent.java
+++ 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamComponent.java
@@ -33,8 +33,6 @@ import org.apache.camel.support.HealthCheckComponent;
 @Component("google-mail-stream")
 public class GoogleMailStreamComponent extends HealthCheckComponent {
 
-    @Metadata(label = "advanced")
-    private Gmail client;
     @Metadata(label = "advanced")
     private GoogleMailClientFactory clientFactory;
     @Metadata(label = "advanced")
@@ -49,20 +47,24 @@ public class GoogleMailStreamComponent extends 
HealthCheckComponent {
         this.configuration = new GoogleMailStreamConfiguration();
     }
 
-    public Gmail getClient(GoogleMailStreamConfiguration 
googleMailConfiguration) {
-        if (client == null) {
-            if (googleMailConfiguration.getClientSecret() != null) {
-                client = 
getClientFactory().makeClient(googleMailConfiguration.getClientId(),
-                        googleMailConfiguration.getClientSecret(), 
googleMailConfiguration.getScopesAsList(),
-                        googleMailConfiguration.getApplicationName(), 
googleMailConfiguration.getRefreshToken(),
-                        googleMailConfiguration.getAccessToken());
-            } else if (googleMailConfiguration.getServiceAccountKey() != null) 
{
-                client = getClientFactory().makeClient(getCamelContext(), 
googleMailConfiguration.getServiceAccountKey(),
-                        googleMailConfiguration.getScopesAsList(), 
googleMailConfiguration.getApplicationName(),
-                        googleMailConfiguration.getDelegate());
-            }
+    /**
+     * Creates a client for the given configuration with the client factory of 
the component. Each endpoint creates its
+     * own client from its own options and keeps it for its lifetime.
+     */
+    public Gmail getClient(GoogleMailStreamConfiguration config) {
+        if (config.getClientSecret() != null) {
+            return getClientFactory().makeClient(config.getClientId(),
+                    config.getClientSecret(), config.getScopesAsList(),
+                    config.getApplicationName(), config.getRefreshToken(),
+                    config.getAccessToken());
+        } else if (config.getServiceAccountKey() != null) {
+            return getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
+                    config.getScopesAsList(), config.getApplicationName(),
+                    config.getDelegate());
+        } else {
+            throw new IllegalArgumentException(
+                    "(clientId and clientSecret) or serviceAccountKey are 
required to create Gmail client");
         }
-        return client;
     }
 
     /**
diff --git 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamEndpoint.java
 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamEndpoint.java
index 3fa4d9d368ec..9888ccb7e15e 100644
--- 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamEndpoint.java
+++ 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamEndpoint.java
@@ -18,6 +18,8 @@ package org.apache.camel.component.google.mail.stream;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import com.google.api.services.gmail.Gmail;
 import com.google.api.services.gmail.model.Label;
@@ -47,6 +49,8 @@ public class GoogleMailStreamEndpoint extends 
ScheduledPollEndpoint implements E
 
     @UriParam
     private GoogleMailStreamConfiguration configuration;
+    private final Lock clientLock = new ReentrantLock();
+    private volatile Gmail client;
 
     public GoogleMailStreamEndpoint(String uri, GoogleMailStreamComponent 
component,
                                     GoogleMailStreamConfiguration 
endpointConfiguration) {
@@ -86,8 +90,24 @@ public class GoogleMailStreamEndpoint extends 
ScheduledPollEndpoint implements E
         return consumer;
     }
 
+    /**
+     * The client of this endpoint, created on first use from the options of 
this endpoint and kept for the lifetime of
+     * the endpoint.
+     */
     public Gmail getClient() {
-        return ((GoogleMailStreamComponent) 
getComponent()).getClient(configuration);
+        Gmail answer = client;
+        if (answer == null) {
+            clientLock.lock();
+            try {
+                if (client == null) {
+                    client = ((GoogleMailStreamComponent) 
getComponent()).getClient(configuration);
+                }
+                answer = client;
+            } finally {
+                clientLock.unlock();
+            }
+        }
+        return answer;
     }
 
     public GoogleMailClientFactory getClientFactory() {
diff --git 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java
 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java
index 6dcf63b8cd70..d09e91228156 100644
--- 
a/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java
+++ 
b/components/camel-google/camel-google-mail/src/main/java/org/apache/camel/component/google/mail/transform/GoogleMailUpdateMessageLabelsDataTypeTransformer.java
@@ -128,6 +128,6 @@ public class 
GoogleMailUpdateMessageLabelsDataTypeTransformer extends Transforme
 
     private Gmail getGmailClient() {
         GoogleMailComponent component = 
getCamelContext().getComponent("google-mail", GoogleMailComponent.class);
-        return component.getClient(component.getConfiguration());
+        return component.getClient();
     }
 }
diff --git 
a/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/GoogleMailClientPerEndpointOptionsTest.java
 
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/GoogleMailClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..4e900ef1150a
--- /dev/null
+++ 
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/GoogleMailClientPerEndpointOptionsTest.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.google.mail;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.services.gmail.Gmail;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleMailClientPerEndpointOptionsTest {
+
+    private static final String OAUTH = 
"?clientId=id&clientSecret=secret&refreshToken=";
+    private static final String SERVICE_ACCOUNT = 
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+    @Test
+    void shouldCreateOneClientPerEndpoint() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-mail", 
GoogleMailComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleMailEndpoint first
+                    = context.getEndpoint("google-mail://messages/list" + 
OAUTH + "token-a", GoogleMailEndpoint.class);
+            GoogleMailEndpoint second
+                    = context.getEndpoint("google-mail://messages/list" + 
OAUTH + "token-b", GoogleMailEndpoint.class);
+            GoogleMailEndpoint sameAsFirst
+                    = context.getEndpoint("google-mail://messages/get" + OAUTH 
+ "token-a", GoogleMailEndpoint.class);
+
+            assertEquals(Arrays.asList("token-a", "token-b", "token-a"), 
factory.created);
+            assertNotSame(first.getClient(), second.getClient());
+            assertNotSame(first.getClient(), sameAsFirst.getClient());
+            assertSame(first.getClient(), first.getClient());
+            assertEquals(3, factory.created.size());
+        }
+    }
+
+    @Test
+    void shouldKeepOneClientPerDelegate() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-mail", 
GoogleMailComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleMailEndpoint first = 
context.getEndpoint("google-mail://messages/list" + SERVICE_ACCOUNT + "user-a",
+                    GoogleMailEndpoint.class);
+            GoogleMailEndpoint second = 
context.getEndpoint("google-mail://messages/list" + SERVICE_ACCOUNT + "user-b",
+                    GoogleMailEndpoint.class);
+
+            assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+            assertNotSame(first.getClient(), second.getClient());
+        }
+    }
+
+    @Test
+    void shouldKeepOneClientForTheComponentConfiguration() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            GoogleMailComponent component = 
context.getComponent("google-mail", GoogleMailComponent.class);
+            component.setClientFactory(factory);
+            GoogleMailConfiguration configuration = new 
GoogleMailConfiguration();
+            configuration.setClientId("id");
+            configuration.setClientSecret("secret");
+            configuration.setRefreshToken("token-component");
+            component.setConfiguration(configuration);
+            context.start();
+
+            Gmail componentClient = component.getClient();
+            GoogleMailEndpoint endpoint
+                    = context.getEndpoint("google-mail://messages/list" + 
OAUTH + "token-a", GoogleMailEndpoint.class);
+
+            assertSame(componentClient, component.getClient());
+            assertNotSame(componentClient, endpoint.getClient());
+            assertEquals(List.of("token-component", "token-a"), 
factory.created);
+        }
+    }
+
+    private static final class RecordingClientFactory implements 
GoogleMailClientFactory {
+
+        private final List<String> created = new ArrayList<>();
+
+        @Override
+        public Gmail makeClient(
+                String clientId, String clientSecret, Collection<String> 
scopes, String applicationName,
+                String refreshToken, String accessToken) {
+            created.add(refreshToken);
+            return newClient();
+        }
+
+        @Override
+        public Gmail makeClient(
+                CamelContext camelContext, String serviceAccountKey, 
Collection<String> scopes, String applicationName,
+                String delegate) {
+            created.add(delegate);
+            return newClient();
+        }
+
+        private static Gmail newClient() {
+            return new Gmail.Builder(new NetHttpTransport(), 
GsonFactory.getDefaultInstance(), null)
+                    .setApplicationName("test").build();
+        }
+    }
+}
diff --git 
a/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamClientPerEndpointOptionsTest.java
 
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..3573d822652d
--- /dev/null
+++ 
b/components/camel-google/camel-google-mail/src/test/java/org/apache/camel/component/google/mail/stream/GoogleMailStreamClientPerEndpointOptionsTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.google.mail.stream;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.services.gmail.Gmail;
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.google.mail.GoogleMailClientFactory;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class GoogleMailStreamClientPerEndpointOptionsTest {
+
+    private static final String OAUTH = 
"?clientId=id&clientSecret=secret&refreshToken=";
+    private static final String SERVICE_ACCOUNT = 
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+    @Test
+    void shouldCreateOneClientPerEndpoint() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-mail-stream", 
GoogleMailStreamComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleMailStreamEndpoint first = 
context.getEndpoint("google-mail-stream://box-a" + OAUTH + "token-a",
+                    GoogleMailStreamEndpoint.class);
+            GoogleMailStreamEndpoint second = 
context.getEndpoint("google-mail-stream://box-b" + OAUTH + "token-b",
+                    GoogleMailStreamEndpoint.class);
+            GoogleMailStreamEndpoint sameAsFirst = 
context.getEndpoint("google-mail-stream://box-c" + OAUTH + "token-a",
+                    GoogleMailStreamEndpoint.class);
+
+            Gmail firstClient = first.getClient();
+            Gmail secondClient = second.getClient();
+
+            assertNotSame(firstClient, secondClient);
+            assertNotSame(firstClient, sameAsFirst.getClient());
+            assertSame(firstClient, first.getClient());
+            assertEquals(Arrays.asList("token-a", "token-b", "token-a"), 
factory.created);
+        }
+    }
+
+    @Test
+    void shouldKeepOneClientPerDelegate() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-mail-stream", 
GoogleMailStreamComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleMailStreamEndpoint first = 
context.getEndpoint("google-mail-stream://box-a" + SERVICE_ACCOUNT + "user-a",
+                    GoogleMailStreamEndpoint.class);
+            GoogleMailStreamEndpoint second = 
context.getEndpoint("google-mail-stream://box-b" + SERVICE_ACCOUNT + "user-b",
+                    GoogleMailStreamEndpoint.class);
+
+            assertNotSame(first.getClient(), second.getClient());
+            assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+        }
+    }
+
+    @Test
+    void shouldFailWithoutCredentials() throws Exception {
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-mail-stream", 
GoogleMailStreamComponent.class)
+                    .setClientFactory(new RecordingClientFactory());
+            context.start();
+
+            GoogleMailStreamEndpoint endpoint
+                    = context.getEndpoint("google-mail-stream://box-a", 
GoogleMailStreamEndpoint.class);
+
+            IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class, endpoint::getClient);
+            assertTrue(e.getMessage().contains("serviceAccountKey"), 
e.getMessage());
+        }
+    }
+
+    private static final class RecordingClientFactory implements 
GoogleMailClientFactory {
+
+        private final List<String> created = new ArrayList<>();
+
+        @Override
+        public Gmail makeClient(
+                String clientId, String clientSecret, Collection<String> 
scopes, String applicationName,
+                String refreshToken, String accessToken) {
+            created.add(refreshToken);
+            return newClient();
+        }
+
+        @Override
+        public Gmail makeClient(
+                CamelContext camelContext, String serviceAccountKey, 
Collection<String> scopes, String applicationName,
+                String delegate) {
+            created.add(delegate);
+            return newClient();
+        }
+
+        private static Gmail newClient() {
+            return new Gmail.Builder(new NetHttpTransport(), 
GsonFactory.getDefaultInstance(), null)
+                    .setApplicationName("test").build();
+        }
+    }
+}
diff --git 
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java
 
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java
index 463946c21276..f3a06901580f 100644
--- 
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java
+++ 
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsComponent.java
@@ -33,8 +33,6 @@ public class GoogleSheetsComponent
     @Metadata
     GoogleSheetsConfiguration configuration;
     @Metadata(label = "advanced")
-    private Sheets client;
-    @Metadata(label = "advanced")
     private GoogleSheetsClientFactory clientFactory;
 
     public GoogleSheetsComponent() {
@@ -50,20 +48,20 @@ public class GoogleSheetsComponent
         return 
getCamelContext().getTypeConverter().convertTo(GoogleSheetsApiName.class, 
apiNameStr);
     }
 
+    /**
+     * Creates a client for the given configuration with the client factory of 
the component. Each endpoint creates its
+     * own client from its own options and keeps it for its lifetime.
+     */
     public Sheets getClient(GoogleSheetsConfiguration config) {
-        if (client == null) {
-            if (config.getClientId() != null && !config.getClientId().isBlank()
-                    && config.getClientSecret() != null && 
!config.getClientSecret().isBlank()) {
-                client = getClientFactory().makeClient(config.getClientId(),
-                        config.getClientSecret(), config.getScopesAsList(),
-                        config.getApplicationName(), config.getRefreshToken(), 
config.getAccessToken());
-            } else {
-                // Use service account key or fall back to Application Default 
Credentials
-                client = getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
-                        config.getScopesAsList(), config.getApplicationName(), 
config.getDelegate());
-            }
+        if (config.getClientId() != null && !config.getClientId().isBlank()
+                && config.getClientSecret() != null && 
!config.getClientSecret().isBlank()) {
+            return getClientFactory().makeClient(config.getClientId(),
+                    config.getClientSecret(), config.getScopesAsList(),
+                    config.getApplicationName(), config.getRefreshToken(), 
config.getAccessToken());
         }
-        return client;
+        // Use service account key or fall back to Application Default 
Credentials
+        return getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
+                config.getScopesAsList(), config.getApplicationName(), 
config.getDelegate());
     }
 
     public GoogleSheetsClientFactory getClientFactory() {
diff --git 
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java
 
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java
index e8b3065dc7e2..334cce5fdcd8 100644
--- 
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java
+++ 
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/GoogleSheetsEndpoint.java
@@ -17,6 +17,8 @@
 package org.apache.camel.component.google.sheets;
 
 import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import com.google.api.services.sheets.v4.Sheets;
 import org.apache.camel.Category;
@@ -46,6 +48,8 @@ public class GoogleSheetsEndpoint extends 
AbstractApiEndpoint<GoogleSheetsApiNam
 
     @UriParam
     private GoogleSheetsConfiguration configuration;
+    private final Lock clientLock = new ReentrantLock();
+    private volatile Sheets client;
 
     private Object apiProxy;
 
@@ -96,8 +100,24 @@ public class GoogleSheetsEndpoint extends 
AbstractApiEndpoint<GoogleSheetsApiNam
         }
     }
 
+    /**
+     * The client of this endpoint, created on first use from the options of 
this endpoint and kept for the lifetime of
+     * the endpoint.
+     */
     public Sheets getClient() {
-        return ((GoogleSheetsComponent) 
getComponent()).getClient(configuration);
+        Sheets answer = client;
+        if (answer == null) {
+            clientLock.lock();
+            try {
+                if (client == null) {
+                    client = ((GoogleSheetsComponent) 
getComponent()).getClient(configuration);
+                }
+                answer = client;
+            } finally {
+                clientLock.unlock();
+            }
+        }
+        return answer;
     }
 
     @Override
diff --git 
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java
 
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java
index 9a0dfbf74030..ab7ac9e58fa4 100644
--- 
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java
+++ 
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamComponent.java
@@ -34,8 +34,6 @@ public class GoogleSheetsStreamComponent extends 
HealthCheckComponent {
     @Metadata
     private GoogleSheetsStreamConfiguration configuration;
     @Metadata(label = "advanced")
-    private Sheets client;
-    @Metadata(label = "advanced")
     private GoogleSheetsClientFactory clientFactory;
 
     public GoogleSheetsStreamComponent() {
@@ -47,19 +45,19 @@ public class GoogleSheetsStreamComponent extends 
HealthCheckComponent {
         this.configuration = new GoogleSheetsStreamConfiguration();
     }
 
+    /**
+     * Creates a client for the given configuration with the client factory of 
the component. Each endpoint creates its
+     * own client from its own options and keeps it for its lifetime.
+     */
     public Sheets getClient(GoogleSheetsStreamConfiguration config) {
-        if (client == null) {
-            if (config.getClientId() != null && config.getClientSecret() != 
null) {
-                client = getClientFactory().makeClient(config.getClientId(),
-                        config.getClientSecret(), config.getScopesAsList(),
-                        config.getApplicationName(), config.getRefreshToken(), 
config.getAccessToken());
-            } else {
-                // Use service account key or fall back to Application Default 
Credentials
-                client = getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
-                        config.getScopesAsList(), config.getApplicationName(), 
config.getDelegate());
-            }
+        if (config.getClientId() != null && config.getClientSecret() != null) {
+            return getClientFactory().makeClient(config.getClientId(),
+                    config.getClientSecret(), config.getScopesAsList(),
+                    config.getApplicationName(), config.getRefreshToken(), 
config.getAccessToken());
         }
-        return client;
+        // Use service account key or fall back to Application Default 
Credentials
+        return getClientFactory().makeClient(getCamelContext(), 
config.getServiceAccountKey(),
+                config.getScopesAsList(), config.getApplicationName(), 
config.getDelegate());
     }
 
     public GoogleSheetsClientFactory getClientFactory() {
diff --git 
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java
 
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java
index dced6726c989..ed0d15bb6fe0 100644
--- 
a/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java
+++ 
b/components/camel-google/camel-google-sheets/src/main/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamEndpoint.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.component.google.sheets.stream;
 
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
 import com.google.api.services.sheets.v4.Sheets;
 import org.apache.camel.Category;
 import org.apache.camel.Consumer;
@@ -41,6 +44,8 @@ public class GoogleSheetsStreamEndpoint extends 
ScheduledPollEndpoint implements
 
     @UriParam
     private GoogleSheetsStreamConfiguration configuration;
+    private final Lock clientLock = new ReentrantLock();
+    private volatile Sheets client;
 
     public GoogleSheetsStreamEndpoint(String uri, GoogleSheetsStreamComponent 
component,
                                       GoogleSheetsStreamConfiguration 
endpointConfiguration) {
@@ -60,8 +65,24 @@ public class GoogleSheetsStreamEndpoint extends 
ScheduledPollEndpoint implements
         return consumer;
     }
 
+    /**
+     * The client of this endpoint, created on first use from the options of 
this endpoint and kept for the lifetime of
+     * the endpoint.
+     */
     public Sheets getClient() {
-        return ((GoogleSheetsStreamComponent) 
getComponent()).getClient(configuration);
+        Sheets answer = client;
+        if (answer == null) {
+            clientLock.lock();
+            try {
+                if (client == null) {
+                    client = ((GoogleSheetsStreamComponent) 
getComponent()).getClient(configuration);
+                }
+                answer = client;
+            } finally {
+                clientLock.unlock();
+            }
+        }
+        return answer;
     }
 
     public GoogleSheetsClientFactory getClientFactory() {
diff --git 
a/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsClientPerEndpointOptionsTest.java
 
b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..7f66f3feec15
--- /dev/null
+++ 
b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsClientPerEndpointOptionsTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.google.sheets;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.sheets.v4.Sheets;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleSheetsClientPerEndpointOptionsTest {
+
+    private static final String OAUTH = 
"?clientId=id&clientSecret=secret&refreshToken=";
+
+    @Test
+    void shouldCreateOneClientPerEndpoint() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-sheets", 
GoogleSheetsComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleSheetsEndpoint first = 
context.getEndpoint("google-sheets://spreadsheets/get" + OAUTH + "token-a",
+                    GoogleSheetsEndpoint.class);
+            GoogleSheetsEndpoint second = 
context.getEndpoint("google-sheets://spreadsheets/get" + OAUTH + "token-b",
+                    GoogleSheetsEndpoint.class);
+            GoogleSheetsEndpoint sameAsFirst = context.getEndpoint(
+                    "google-sheets://spreadsheets/create" + OAUTH + "token-a", 
GoogleSheetsEndpoint.class);
+
+            assertEquals(Arrays.asList("token-a", "token-b", "token-a"), 
factory.created);
+            assertNotSame(first.getClient(), second.getClient());
+            assertNotSame(first.getClient(), sameAsFirst.getClient());
+            assertSame(first.getClient(), first.getClient());
+            assertEquals(3, factory.created.size());
+        }
+    }
+
+    @Test
+    void shouldNotShareTheDefaultCredentialsClient() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-sheets", 
GoogleSheetsComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleSheetsEndpoint defaultCredentials
+                    = context.getEndpoint("google-sheets://spreadsheets/get", 
GoogleSheetsEndpoint.class);
+            GoogleSheetsEndpoint explicitCredentials = context.getEndpoint(
+                    "google-sheets://spreadsheets/get" + OAUTH + "token-b", 
GoogleSheetsEndpoint.class);
+
+            assertEquals(Arrays.asList(null, "token-b"), factory.created);
+            assertNotSame(defaultCredentials.getClient(), 
explicitCredentials.getClient());
+        }
+    }
+
+    private static final class RecordingClientFactory implements 
GoogleSheetsClientFactory {
+
+        private final List<String> created = new ArrayList<>();
+
+        @Override
+        public Sheets makeClient(
+                String clientId, String clientSecret, Collection<String> 
scopes, String applicationName,
+                String refreshToken, String accessToken) {
+            created.add(refreshToken);
+            return newClient();
+        }
+
+        @Override
+        public Sheets makeClient(
+                CamelContext camelContext, String serviceAccountKey, 
Collection<String> scopes, String applicationName,
+                String delegate) {
+            created.add(delegate);
+            return newClient();
+        }
+
+        private static Sheets newClient() {
+            return new Sheets.Builder(new NetHttpTransport(), new 
JacksonFactory(), null).setApplicationName("test")
+                    .build();
+        }
+    }
+}
diff --git 
a/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamClientPerEndpointOptionsTest.java
 
b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamClientPerEndpointOptionsTest.java
new file mode 100644
index 000000000000..c8039e19f7db
--- /dev/null
+++ 
b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/stream/GoogleSheetsStreamClientPerEndpointOptionsTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.google.sheets.stream;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.sheets.v4.Sheets;
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.google.sheets.GoogleSheetsClientFactory;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class GoogleSheetsStreamClientPerEndpointOptionsTest {
+
+    private static final String OAUTH = 
"?clientId=id&clientSecret=secret&refreshToken=";
+    private static final String SERVICE_ACCOUNT = 
"?serviceAccountKey=classpath:service-account.json&delegate=";
+
+    @Test
+    void shouldCreateOneClientPerEndpoint() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-sheets-stream", 
GoogleSheetsStreamComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleSheetsStreamEndpoint first = 
context.getEndpoint("google-sheets-stream://sheet-a" + OAUTH + "token-a",
+                    GoogleSheetsStreamEndpoint.class);
+            GoogleSheetsStreamEndpoint second = 
context.getEndpoint("google-sheets-stream://sheet-b" + OAUTH + "token-b",
+                    GoogleSheetsStreamEndpoint.class);
+            GoogleSheetsStreamEndpoint sameAsFirst = context.getEndpoint(
+                    "google-sheets-stream://sheet-c" + OAUTH + "token-a", 
GoogleSheetsStreamEndpoint.class);
+
+            Sheets firstClient = first.getClient();
+            Sheets secondClient = second.getClient();
+
+            assertNotSame(firstClient, secondClient);
+            assertNotSame(firstClient, sameAsFirst.getClient());
+            assertSame(firstClient, first.getClient());
+            assertEquals(Arrays.asList("token-a", "token-b", "token-a"), 
factory.created);
+        }
+    }
+
+    @Test
+    void shouldKeepOneClientPerDelegate() throws Exception {
+        RecordingClientFactory factory = new RecordingClientFactory();
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.getComponent("google-sheets-stream", 
GoogleSheetsStreamComponent.class).setClientFactory(factory);
+            context.start();
+
+            GoogleSheetsStreamEndpoint first
+                    = context.getEndpoint("google-sheets-stream://sheet-a" + 
SERVICE_ACCOUNT + "user-a",
+                            GoogleSheetsStreamEndpoint.class);
+            GoogleSheetsStreamEndpoint second
+                    = context.getEndpoint("google-sheets-stream://sheet-b" + 
SERVICE_ACCOUNT + "user-b",
+                            GoogleSheetsStreamEndpoint.class);
+
+            assertNotSame(first.getClient(), second.getClient());
+            assertEquals(Arrays.asList("user-a", "user-b"), factory.created);
+        }
+    }
+
+    private static final class RecordingClientFactory implements 
GoogleSheetsClientFactory {
+
+        private final List<String> created = new ArrayList<>();
+
+        @Override
+        public Sheets makeClient(
+                String clientId, String clientSecret, Collection<String> 
scopes, String applicationName,
+                String refreshToken, String accessToken) {
+            created.add(refreshToken);
+            return newClient();
+        }
+
+        @Override
+        public Sheets makeClient(
+                CamelContext camelContext, String serviceAccountKey, 
Collection<String> scopes, String applicationName,
+                String delegate) {
+            created.add(delegate);
+            return newClient();
+        }
+
+        private static Sheets newClient() {
+            return new Sheets.Builder(new NetHttpTransport(), new 
JacksonFactory(), null).setApplicationName("test")
+                    .build();
+        }
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index eb7e7c67263c..52f22d3093dc 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -232,6 +232,21 @@ a JSON object when none is configured). A route that sets 
the header must enable
 `.unmarshal(new JsonDataFormat(JsonLibrary.Gson).allowUnmarshallType(true))` or
 `<json library="Gson" allowUnmarshallType="true"/>`.
 
+=== camel-google-mail, camel-google-drive, camel-google-calendar, 
camel-google-sheets - one API client per endpoint
+
+The `google-mail`, `google-mail-stream`, `google-drive`, `google-calendar`, 
`google-calendar-stream`, `google-sheets`
+and `google-sheets-stream` components now create one API client per endpoint, 
from the options of that endpoint
+(`clientId`, `clientSecret`, `refreshToken`, `accessToken`, 
`serviceAccountKey`, `delegate`, `scopes` and
+`applicationName`, plus `emailAddress`, `p12FileName` and `user` on the two 
calendar components), instead of a single
+client per component that every endpoint of the component shared. Each 
endpoint keeps its client for its lifetime. The
+`clientFactory` option of a `google-drive` endpoint is now used to create the 
client of that endpoint. In addition, a
+`google-mail-stream` endpoint configured with neither `clientSecret` nor 
`serviceAccountKey` now fails with an
+`IllegalArgumentException` when its client is created, instead of getting a 
`null` client and failing at the first API
+call. The `google-mail:update-message-labels` data type transformer now uses a 
client created from the component-level
+configuration (the `camel.component.google-mail.*` options), kept until the 
component stops. Before, it used whichever
+client the component had already created, for example the one of a 
`google-mail:messages/modify` endpoint, so
+credentials set only on an endpoint URI no longer reach the transformer: set 
them on the component as well.
+
 === camel-jbang - camel validate and camel overview report the same endpoints 
no route consumes
 
 `camel validate source` and `camel overview` now use the same rules for a 
`direct:`, `seda:` or `disruptor:` endpoint

Reply via email to