Repository: quarkusio/quarkus
Issue: Rest Client exausts Threads. 

### Describe the bug

This is a major issue for us.
I have dozens of services in prod that from time to time they freeze due to thread exhaustion. The vertex event loop stalls. 
his can be traced back to unconfigured timeouts on rest clients that hog connections. 
We need to go to all rest clients and specify connection and read timeouts on configurations, like:
```
com.td.wfm.common.client.SomeRestClientClass/mp-rest/connectTimeout=500
com.td.wfm.common.client.SomeRestClientClass/mp-rest/readTimeout=1000
```
If you use programatic rest creation you must:
```
        return RestClientBuilder.newBuilder()
                                .baseUri(URI.create(uri))
                                .connectTimeout(connectionTimeout, TimeUnit.MILLISECONDS)
                                .readTimeout(readTimeout, TimeUnit.MILLISECONDS)
                                //...
```

There are many issues related with this. See:
https://github.com/quarkusio/quarkus/issues/15056
https://github.com/quarkusio/quarkus/issues/15383
https://github.com/quarkusio/quarkus/issues/15191

This happens on 1.13.7.Final but I believe 2.* suffers from the same problem.

### Expected behavior

If the REST Client connection is half closed in the other side, it will timeout after a default amount of time. 

### Actual behavior

If the REST Client connection is half closed in the other side it will never timeout.

### How to Reproduce?

You need a rest client calling an endpoint in a loop, in different threads. The other side must hold the connection and not reply. 

### Output of `uname -a` or `ver`

_No response_

### Output of `java -version`

11

### GraalVM version (if different from Java)

_No response_

### Quarkus version or git rev

1.13.7.Final

### Build tool (ie. output of `mvnw --version` or `gradlew --version`)

_No response_

### Additional information

_No response_

Changed files:
extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java
extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java
extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java
extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java
extensions/resteasy-classic/rest-client/runtime/src/test/java/io/quarkus/restclient/runtime/RestClientBaseTest.java

diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java
index 10e0d804735..ce58a3a5bf0 100644
--- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java
+++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java
@@ -95,6 +95,20 @@ public class RestClientsConfig {
 
     public RestClientLoggingConfig logging;
 
+    /**
+     * Global default connect timeout for automatically generated REST Clients. The attribute specifies a timeout
+     * in milliseconds that a client should wait to connect to the remote endpoint.
+     */
+    @ConfigItem(defaultValue = "15000", defaultValueDocumentation = "15000 ms")
+    public Long connectTimeout;
+
+    /**
+     * Global default read timeout for automatically generated REST Clients. The attribute specifies a timeout
+     * in milliseconds that a client should wait for a response from the remote endpoint.
+     */
+    @ConfigItem(defaultValue = "30000", defaultValueDocumentation = "30000 ms")
+    public Long readTimeout;
+
     public RestClientConfig getClientConfig(String configKey) {
         if (configKey == null) {
             return RestClientConfig.EMPTY;
diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java
index 5a00ee54881..4600ff79c6f 100644
--- a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java
+++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java
@@ -269,16 +269,16 @@ private Class<?> providerClassForName(String name) {
     }
 
     void configureTimeouts(RestClientBuilder builder) {
-        Optional<Long> connectTimeout = oneOf(clientConfigByClassName().connectTimeout,
-                clientConfigByConfigKey().connectTimeout);
-        if (connectTimeout.isPresent()) {
-            builder.connectTimeout(connectTimeout.get(), TimeUnit.MILLISECONDS);
+        Long connectTimeout = oneOf(clientConfigByClassName().connectTimeout,
+                clientConfigByConfigKey().connectTimeout).orElse(this.configRoot.connectTimeout);
+        if (connectTimeout != null) {
+            builder.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS);
         }
 
-        Optional<Long> readTimeout = oneOf(clientConfigByClassName().readTimeout,
-                clientConfigByConfigKey().readTimeout);
-        if (readTimeout.isPresent()) {
-            builder.readTimeout(readTimeout.get(), TimeUnit.MILLISECONDS);
+        Long readTimeout = oneOf(clientConfigByClassName().readTimeout,
+                clientConfigByConfigKey().readTimeout).orElse(this.configRoot.readTimeout);
+        if (readTimeout != null) {
+            builder.readTimeout(readTimeout, TimeUnit.MILLISECONDS);
         }
     }
 
@@ -322,10 +322,13 @@ private RestClientConfig clientConfigByClassName() {
         return this.configRoot.getClientConfig(this.proxyType);
     }
 
-    private static <T> Optional<T> oneOf(Optional<T> o1, Optional<T> o2) {
-        if (o1.isPresent()) {
-            return o1;
+    @SafeVarargs
+    private static <T> Optional<T> oneOf(Optional<T>... optionals) {
+        for (Optional<T> o : optionals) {
+            if (o.isPresent()) {
+                return o;
+            }
         }
-        return o2;
+        return Optional.empty();
     }
 }
diff --git a/extensions/resteasy-classic/rest-client/runtime/src/test/java/io/quarkus/restclient/runtime/RestClientBaseTest.java b/extensions/resteasy-classic/rest-client/runtime/src/test/java/io/quarkus/restclient/runtime/RestClientBaseTest.java
index b8d330756d2..149560eb383 100644
--- a/extensions/resteasy-classic/rest-client/runtime/src/test/java/io/quarkus/restclient/runtime/RestClientBaseTest.java
+++ b/extensions/resteasy-classic/rest-client/runtime/src/test/java/io/quarkus/restclient/runtime/RestClientBaseTest.java
@@ -99,6 +99,23 @@ public void testQuarkusConfig() throws Exception {
         Mockito.verify(restClientBuilderMock).property("resteasy.connectionPoolSize", 103);
     }
 
+    @Test
+    public void testGlobalTimeouts() {
+        RestClientsConfig configRoot = new RestClientsConfig();
+        configRoot.connectTimeout = 5000L;
+        configRoot.readTimeout = 10000L;
+        RestClientBuilder restClientBuilderMock = Mockito.mock(RestClientBuilder.class);
+        RestClientBase restClientBase = new RestClientBase(TestClient.class,
+                "http://localhost:8080",
+                "test-client",
+                null,
+                configRoot);
+        restClientBase.configureTimeouts(restClientBuilderMock);
+
+        Mockito.verify(restClientBuilderMock).connectTimeout(5000, TimeUnit.MILLISECONDS);
+        Mockito.verify(restClientBuilderMock).readTimeout(10000, TimeUnit.MILLISECONDS);
+    }
+
     /**
      * This method creates a Quarkus style configuration object (which would normally be created based on the MP config
      * properties, but it's easier to instantiate it directly here).
diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java
index 1f3fb2ee0c6..aa3e93ed981 100644
--- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java
+++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java
@@ -310,16 +310,16 @@ private Class<?> providerClassForName(String name) {
     }
 
     private void configureTimeouts(RestClientBuilder builder) {
-        Optional<Long> connectTimeout = oneOf(clientConfigByClassName().connectTimeout,
-                clientConfigByConfigKey().connectTimeout);
-        if (connectTimeout.isPresent()) {
-            builder.connectTimeout(connectTimeout.get(), TimeUnit.MILLISECONDS);
+        Long connectTimeout = oneOf(clientConfigByClassName().connectTimeout,
+                clientConfigByConfigKey().connectTimeout).orElse(this.configRoot.connectTimeout);
+        if (connectTimeout != null) {
+            builder.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS);
         }
 
-        Optional<Long> readTimeout = oneOf(clientConfigByClassName().readTimeout,
-                clientConfigByConfigKey().readTimeout);
-        if (readTimeout.isPresent()) {
-            builder.readTimeout(readTimeout.get(), TimeUnit.MILLISECONDS);
+        Long readTimeout = oneOf(clientConfigByClassName().readTimeout,
+                clientConfigByConfigKey().readTimeout).orElse(this.configRoot.readTimeout);
+        if (readTimeout != null) {
+            builder.readTimeout(readTimeout, TimeUnit.MILLISECONDS);
         }
     }
 
@@ -359,10 +359,13 @@ private RestClientConfig clientConfigByClassName() {
         return this.configRoot.getClientConfig(jaxrsInterface);
     }
 
-    private static <T> Optional<T> oneOf(Optional<T> o1, Optional<T> o2) {
-        if (o1.isPresent()) {
-            return o1;
+    @SafeVarargs
+    private static <T> Optional<T> oneOf(Optional<T>... optionals) {
+        for (Optional<T> o : optionals) {
+            if (o.isPresent()) {
+                return o;
+            }
         }
-        return o2;
+        return Optional.empty();
     }
 }
diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java
index a04647b57dc..c43bb7d913d 100644
--- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java
+++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java
@@ -97,6 +97,22 @@ public void testQuarkusConfig() {
                 HttpPostRequestEncoder.EncoderMode.HTML5);
     }
 
+    @Test
+    public void testGlobalTimeouts() {
+        RestClientsConfig configRoot = new RestClientsConfig();
+        configRoot.connectTimeout = 5000L;
+        configRoot.readTimeout = 10000L;
+        configRoot.multipartPostEncoderMode = Optional.empty();
+        RestClientBuilderImpl restClientBuilderMock = Mockito.mock(RestClientBuilderImpl.class);
+        new RestClientCDIDelegateBuilder<>(TestClient.class,
+                "http://localhost:8080",
+                "test-client",
+                configRoot).build(restClientBuilderMock);
+
+        Mockito.verify(restClientBuilderMock).connectTimeout(5000, TimeUnit.MILLISECONDS);
+        Mockito.verify(restClientBuilderMock).readTimeout(10000, TimeUnit.MILLISECONDS);
+    }
+
     private static RestClientsConfig createSampleConfiguration() {
         RestClientConfig clientConfig = new RestClientConfig();
         clientConfig.url = Optional.of("http://localhost");